package tools;

import static org.lwjgl.opengl.GL11.GL_COLOR_ARRAY;
import static org.lwjgl.opengl.GL11.GL_COLOR_BUFFER_BIT;
import static org.lwjgl.opengl.GL11.GL_FLOAT;
import static org.lwjgl.opengl.GL11.GL_FRONT_AND_BACK;
import static org.lwjgl.opengl.GL11.GL_LINES;
import static org.lwjgl.opengl.GL11.GL_POINTS;
import static org.lwjgl.opengl.GL11.GL_POINT;
import static org.lwjgl.opengl.GL11.GL_VERTEX_ARRAY;
import static org.lwjgl.opengl.GL11.glClear;
import static org.lwjgl.opengl.GL11.glColor3f;
import static org.lwjgl.opengl.GL11.glColorPointer;
import static org.lwjgl.opengl.GL11.glDisableClientState;
import static org.lwjgl.opengl.GL11.glDrawArrays;
import static org.lwjgl.opengl.GL11.glEnableClientState;
import static org.lwjgl.opengl.GL11.glPolygonMode;
import static org.lwjgl.opengl.GL11.glPopMatrix;
import static org.lwjgl.opengl.GL11.glPushMatrix;
import static org.lwjgl.opengl.GL11.glRotatef;
import static org.lwjgl.opengl.GL11.glScalef;
import static org.lwjgl.opengl.GL11.glTranslated;
import static org.lwjgl.opengl.GL11.glVertexPointer;
import static org.lwjgl.opengl.GL15.GL_ARRAY_BUFFER;
import static org.lwjgl.opengl.GL15.GL_STATIC_DRAW;
import static org.lwjgl.opengl.GL15.glBindBuffer;
import static org.lwjgl.opengl.GL15.glBufferData;
import static org.lwjgl.opengl.GL15.glGenBuffers;

import java.nio.FloatBuffer;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Random;

import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;

import computation.Clustering;
import computation.Geometry;
import computation.Graphs;

import primatives.PointND;
import util.ClusterEdge;
import util.GeoTriangle;
import util.GeoVertex;

import color.CLRConst;
import color.CLRConvert;
import delaunay_triangulation.Point_dt;
import delaunay_triangulation.Triangle_dt;

public class PointCloudViewer
{
	private static final int VERTEX_SIZE = 3; // used to tell the VBO the number of floats per vertex
	private static final int EDGE_SIZE = 2; // used to tell the VBO the number of vertices per edge
	
	private Random rand;
	
	public enum RENDER_TYPE
	{
		POINTS,
		CLUSTERING,
		MST
	}
	
	private ArrayList<PointND> points;
	private ArrayList<ArrayList<PointND>> clusters;
	private ArrayList<ClusterEdge> MST;
	
	private boolean _renderToRender;
	public boolean updateColors;
	public boolean updateComplete;
	
	public RENDER_TYPE render_mode;
	
	private float _xRot = 0.0f;
	private float _yRot = 0.0f;
	private float _zRot = 0.0f;
	public void rotate(float x, float y, float z)
	{
		_xRot += x;
		_yRot += y;
		_zRot += z;
	}
	
	public void reset_rotations()
	{
		_xRot = 0.0f;
		_yRot = 0.0f;
		_zRot = 0.0f;
	}
	
	public double xBoundMin, xBoundMax, yBoundMin, yBoundMax, zBoundMin, zBoundMax;
	public double getBoundX_Legnth() { return xBoundMax - xBoundMin; }
	public double getBoundY_Legnth() { return yBoundMax - yBoundMin; }
	public double getBoundZ_Legnth() { return zBoundMax - zBoundMin; }
	
	private float MESH_ZOOM;
	public void setMeshScale(float s) { MESH_ZOOM = s; }
	public void zoom(float s) { MESH_ZOOM += s; }
	
	public PointCloudViewer()
	{
		points = null;
		_renderToRender = false;
		
		updateColors = false;
		updateComplete = false;
		
		rand = new Random(System.currentTimeMillis()); // used for generating random colors
		
		MESH_ZOOM = 1.0f; // a zooming variable does not actually modify the mesh
		render_mode = RENDER_TYPE.POINTS;
	}
	
	public boolean isLoaded() { return points != null && points.size() > 0; }
	public ArrayList<PointND> getCurrentPointSet() { return points; }
	
	public void setPoints(ArrayList<PointND>  cloud)
	{
		points = cloud;
		clusters = new ArrayList<ArrayList<PointND>>();
		_renderToRender = false;
		render_mode = RENDER_TYPE.POINTS;
	}
	
	private int vboHandle_Vertices;
	private int vboHandle_Edges;
	private int vboHandle_Colors;
	private FloatBuffer buf_vertices;
	private FloatBuffer buf_edges; // edges for the MST
	private FloatBuffer buf_colors;
	// called once when loading a completely new geometry, needed for VBO rendering
	// returns true if geometry has been created
	public boolean createGeometry()
	{
		if (!_renderToRender) // done once after the DT is completed
		{
			System.out.print("Creating initial OpenGL geometry ... ");

			// create the buffers and maps
			buf_vertices = BufferUtils.createFloatBuffer(points.size() * VERTEX_SIZE); // 3 floats per 1 vertex
			buf_colors = BufferUtils.createFloatBuffer(points.size() * VERTEX_SIZE); // 3 floats per 1 color per 1 vertex
			buf_edges = null; // forget about edges for now as we only need them when we do MST clustering

			for (PointND pt: points)
			{
				float x = (float)pt.get_coord(0);
				float y = (float)pt.get_coord(1);
				float z = (float)pt.get_coord(2);
				
				float[] vertex = {x, y, z};
				buf_vertices.put(vertex);
				
				// normal 3 dimensional points, just xyz, make up a color and stuff it into the color buffer
				if (pt.dimensions == 3)
				{
					buf_colors.put(CLRConst.GREEN);
				}
				
				// ok, xyz and strike + dip, find the color representation and fill up all the buffers
				else if (pt.dimensions == 5)
				{
					double s = pt.get_coord(3);
					double d = pt.get_coord(4);
					float[] clr = CLRConvert.GetStrikeDipColor(s, d);
					buf_colors.put(clr);
				}
				
				// ok, xyz and rgb... fill up all the buffers
				else if (pt.dimensions == 6)
				{
					float r = (float)pt.get_coord(3);
					float g = (float)pt.get_coord(4);
					float b = (float)pt.get_coord(5);
					
					float[] clr = {r, g, b};
					buf_colors.put(clr);
				}
				
				// update bounding box
				if (x > xBoundMax) xBoundMax = x;
				if (x < xBoundMin) xBoundMin = x;
				if (y > yBoundMax) yBoundMax = y;
				if (y < yBoundMin) yBoundMin = y;
				if (z > zBoundMax) zBoundMax = z;
				if (z < zBoundMin) zBoundMin = z;
			}
			buf_vertices.flip();
			buf_colors.flip();

			vboHandle_Vertices = glGenBuffers();
			vboHandle_Colors = glGenBuffers();
			
	        glBindBuffer(GL_ARRAY_BUFFER, vboHandle_Vertices);
	        glBufferData(GL_ARRAY_BUFFER, buf_vertices, GL_STATIC_DRAW);
	        glBindBuffer(GL_ARRAY_BUFFER, vboHandle_Colors);
	        glBufferData(GL_ARRAY_BUFFER, buf_colors, GL_STATIC_DRAW);
	        
	        glBindBuffer(GL_ARRAY_BUFFER, 0);
	        
			_renderToRender = true;
			System.out.println(" done!");
			return true;
		}
		return false;
	}
	
	
	// regenerates all the geometry: vertices, faces and normals
	public void regenerate_complete()
	{
		System.out.print("Regenerating geometry...");
		
		_renderToRender = false;

		// ensure that we de-reference these buffers
		buf_vertices = null;
		buf_colors = null;

		// create the buffers and maps
		buf_vertices = BufferUtils.createFloatBuffer(points.size() * VERTEX_SIZE); // 3 floats per 1 vertex
		buf_colors = BufferUtils.createFloatBuffer(points.size() * VERTEX_SIZE); // 3 floats per 1 color per 1 vertex
		
		
		boolean generate_edges = MST != null && MST.size() > 0; 
		if (generate_edges)
		{
			buf_edges = null;
			buf_edges = BufferUtils.createFloatBuffer(MST.size() * VERTEX_SIZE * EDGE_SIZE); // 3 floats per 1 color per 1 vertex
		}
		
		if (render_mode == RENDER_TYPE.POINTS)
		{
			
		}
		else if (render_mode == RENDER_TYPE.CLUSTERING)
		{
			// find the number of clusters and define a color array so that each cluster gets a "unique" color perhaps on a scale??
			
			for (int i = 0; i < clusters.size(); i++)
			{
				ArrayList<PointND> cluster = clusters.get(i);
				float[] clr = {rand.nextFloat(), rand.nextFloat(), rand.nextFloat()}; 
				
				if (cluster.size() < 0.01*points.size())
				{
					clr = CLRConst.WHITE;
				}
				for (PointND pt: cluster)
				{
					float x = (float)pt.get_coord(0);
					float y = (float)pt.get_coord(1);
					float z = (float)pt.get_coord(2);
					
					float[] vertex = {x, y, z};
					buf_vertices.put(vertex);
					
					buf_colors.put(clr);
					
					// update bounding box
					if (x > xBoundMax) xBoundMax = x;
					if (x < xBoundMin) xBoundMin = x;
					if (y > yBoundMax) yBoundMax = y;
					if (y < yBoundMin) yBoundMin = y;
					if (z > zBoundMax) zBoundMax = z;
					if (z < zBoundMin) zBoundMin = z;
				}
			}
		}
		else if (render_mode == RENDER_TYPE.MST)
		{
			for (ClusterEdge edge: MST)
			{
				if (edge.bRender)
				{
					PointND start = edge.start;
					PointND end = edge.end;
					
					float sx = (float)start.get_coord(0);
					float sy = (float)start.get_coord(1);
					float sz = (float)start.get_coord(2);
					
					float ex = (float)end.get_coord(0);
					float ey = (float)end.get_coord(1);
					float ez = (float)end.get_coord(2);
					
					float[] edge_start = {sx, sy, sz};
					float[] edge_end = {ex, ey, ez};
					
					buf_edges.put(edge_start);
					buf_edges.put(edge_end);
				}
				else
				{
					// place holder, TODO: make this more efficient and find out exactly the number of edges we want and resize the buffer properly
					float[] edge_start = {0.0f, 0.0f, 0.0f};
					float[] edge_end = {0.0f, 0.0f, 0.0f};
					
					buf_edges.put(edge_start);
					buf_edges.put(edge_end);
				}
			}
		}
		
		
		buf_vertices.flip();
		buf_colors.flip();

		vboHandle_Vertices = glGenBuffers();
		vboHandle_Colors = glGenBuffers();
		
        glBindBuffer(GL_ARRAY_BUFFER, vboHandle_Vertices);
        glBufferData(GL_ARRAY_BUFFER, buf_vertices, GL_STATIC_DRAW);
        glBindBuffer(GL_ARRAY_BUFFER, vboHandle_Colors);
        glBufferData(GL_ARRAY_BUFFER, buf_colors, GL_STATIC_DRAW);
        
		if (generate_edges)
		{
			buf_edges.flip();
			vboHandle_Edges = glGenBuffers();
	        glBindBuffer(GL_ARRAY_BUFFER, vboHandle_Edges);
	        glBufferData(GL_ARRAY_BUFFER, buf_edges, GL_STATIC_DRAW);
//	        glBindBuffer(GL_ARRAY_BUFFER, 0);
		}

		_renderToRender = true;
		System.out.println(" done!");
	}
	
	
	public void repaint()
	{
		
	}
	
	public void render()
	{
		glPolygonMode(GL_FRONT_AND_BACK, GL_POINT);
		
		// ensure we are ready to render before we alter the rendering data!
		if (_renderToRender)
		{
			if (updateColors)
			{
				repaint();
				updateColors = false;
			}
			
			if (updateComplete)
			{
				regenerate_complete();
				updateComplete = false;
			}
		}

		glPushMatrix();
		
		float x = (float)(xBoundMin + getBoundX_Legnth()/2);
		float y = (float)(yBoundMin + getBoundY_Legnth()/2);
		float z = (float)(zBoundMin);
		
		glScalef(MESH_ZOOM, MESH_ZOOM, 1.0f);
		glTranslated(x, y, z);
		glRotatef(_zRot, 0.0f, 0.0f, 1.0f);
		glRotatef(_yRot, 0.0f, 1.0f, 0.0f);
		glRotatef(_xRot, 1.0f, 0.0f, 0.0f);
		glTranslated(-x, -y, -z);
		
		render_points();
		
		if  (render_mode == RENDER_TYPE.MST)
		{
			render_edges();
		}
		
		glPopMatrix();
	}

	private void render_points()
	{
		// render using VBO
		if (_renderToRender)
		{
	        glClear(GL_COLOR_BUFFER_BIT);
	        
            glBindBuffer(GL_ARRAY_BUFFER, vboHandle_Vertices);
            glVertexPointer(3, GL_FLOAT, 0, 0L); // vertex size = 3 => 3 floats per vertex
            glBindBuffer(GL_ARRAY_BUFFER, vboHandle_Colors);
            glColorPointer(3, GL_FLOAT, 0, 0L); // color size = 3 => 3 floats per color
            
            glEnableClientState(GL_VERTEX_ARRAY);
            glEnableClientState(GL_COLOR_ARRAY);
            
            glDrawArrays(GL_POINTS, 0, points.size());
            
            glDisableClientState(GL_COLOR_ARRAY);
            glDisableClientState(GL_VERTEX_ARRAY);
		}
	}
	
	public void render_edges()
	{

        if (buf_edges != null) // have edges? render them!
        {
            glBindBuffer(GL_ARRAY_BUFFER, vboHandle_Edges);
            glVertexPointer(3, GL_FLOAT, 0, 0L); // vertex size = 3 => 3 floats per vertex
            
        	glColor3f(1.0f, 1.0f, 1.0f);
        	glEnableClientState(GL_VERTEX_ARRAY);
        	glDrawArrays(GL_LINES, 0, MST.size()*2);
        	glDisableClientState(GL_VERTEX_ARRAY);
        }
	}

	public void find_clusters(double eps, double d)
	{		
		clusters = Clustering.cluster(points, 3, eps, d);
		System.out.println("Found " + clusters.size() + " clusters.");
		
		render_mode = RENDER_TYPE.CLUSTERING;
		updateComplete = true;
	}

	
	public void find_mst()
	{
		MST = Graphs.MST_Euclidean(points, 100);
		System.out.println(MST.size());
	}

	public void mst_clustering(double eps)
	{
		if (MST != null && MST.size() > 0)
		{
			// first compute some stats
			double dMin = Double.MAX_VALUE;
			double dMax = Double.MIN_VALUE;
			double dAvg = 0.0;
			
			for (ClusterEdge edge: MST)
			{
				edge.bRender = false; // reset all the rendering flags, assume nothing renders
				dMin = Math.min(dMin, edge.length);
				dMax = Math.max(dMax, edge.length);
				dAvg += edge.length;
			}
			
			dAvg /= MST.size();
			double dSTD = 0.0;
			for (ClusterEdge edge: MST) dSTD += Math.pow((edge.length - dAvg), 2);
			dSTD /= MST.size();
			dSTD = Math.sqrt(dSTD);
			
			
			System.out.println("MST Clustering Stats:");
			System.out.println("Min: " + dMin);
			System.out.println("Max: " + dMax);
			System.out.println("Avg: " + dAvg);
			System.out.println("STD: " + dSTD);
			System.out.println("Eps: " + eps);
			System.out.print("Dropping all less than Eps*STD greater than and less than the Average... ");
			
			double lower_bound = dAvg - eps*dSTD;
			double upper_bound = dAvg + eps*dSTD;
			
			for (ClusterEdge edge: MST)
			{
//				if (edge.length >= lower_bound && edge.length <= upper_bound) // only render an edge if it is within eps*STD of the average
				if (edge.length <= upper_bound) // only render an edge if it is within eps*STD of the average
					edge.bRender = true;
			}
			
			System.out.println(" ... done!");
			
			updateComplete = true;
			
			
		}
		
	}

}
